package org.mashart.pink.m.gps;

import java.util.Timer;
import java.util.TimerTask;
import org.mashart.pink.m.gui.Gui;
import org.mashart.pink.m.ws.WsConnector;
//import org.mashart.pink.m.ws.RestfulClient;
import org.mashart.pink.m.ws.FileConnector;
//import org.mashart.pink.m.ws.FakeWsConnector;
import org.mashart.pink.m.ws.RobustRestfulClient;
import org.mashart.pink.m.store.Preferences;

/**
 * The main class where half of the magic happens.
 *
 * Initializes GPS device connector and HTTP or File connector, receives GPS data and
 * POST them over HTTP, or save the data as a file in the phone memory.
 *
 * The length of action string plus the length of prefix "action=" can not exceed a limit.
 * See <code>AbstractWsConnector.MAX_LENGTH_ACTION</code>
 */
public class Receiver extends TimerTask implements Runnable {

    /**
     * Our main UI class instance.
     * Again, I'd like to see something like GuiListener here
     */
    private Gui gui;

    /* TODO use buffered input with DataManager
     * Reply: done by using NMEAAssembler, current buffer size is 2
    //private DataManager data;
     */
    /**
     * GPS device connector
     */
    private DeviceConnector connector;
    /**
     * HTTP RESTful client
     */
    private WsConnector ws_client;
    /**
     * true - the receiver is initialized
     */
    private boolean initialized;
    /**
     * true - we're running, receiving and sending GPS data
     */
    private boolean receiving;
    /**
     * true - we've paused, receiving but not sending GPS data
     */
    private boolean sending;
    /**
     * round started
     */
    private boolean roundStarted;
    /**
     * GPS data collection and POSTing interval,
     * milliseconds.
     * @see <code>initialize()</code>
     */
    private final int INTERVAL = 1000;
    /**
     * Timer, schedules the receiver to get a new waypoint periodically
     */
    private Timer timer;
    /**
     * Time counter, counts the time elapsed since receiver is activated.
     * It leaps with the same step as the action interval of the receiver.
     */
    private int runTime;
    /**
     * Timer
     */
    private long tit;
    /**
     * Timer of speed update
     */
    private long spdTit;
    /**
     * timer of below minimum valid speed
     */
    private long belowSpeedTit;
    /**
     * time out of below speed, will triger auto-pause
     */
    private final int TIMEOUT_BELOW_SPEED = 5000;
    /**
     * Accumulated distance
     */
    private double dist;
    /**
     * Average speed
     */
    private float aveSpeed;
    /**
     * Max speed
     */
    private float maxSpeed;
    /**
     * max valid speed
     */
    private final float minValidSpeed;
    /**
     * Latitiude of the starting waypoint
     */
    private double startLat;
    /**
     * Longitude of the starting waypoint
     */
    private double startLon;
    /**
     * if auto-pause is enabled
     */
    private final boolean autoPauseMode;
    /**
     * auto paused
     */
    private boolean autoPaused;
    /**
     * semaphore
     */
    final Object sem;

    /**
     * Buffer of received waypoints. Whenever updated, it will decide whether/when to send the waypoint to the web server.
     * WaypointsBuffer implements the waypoints reduction algorithm.
     * WaypointsBuffer is optional.
     * If in use, it is invoked in: initialize(), run(), terminate()
     * If not in use, the simple ws_client.push() is used instead.
     */
//    private WaypointsBuffer wpBuffer;
    /**
     * Constructor. Instantiates BT device connector,
     * HTTP client and NMEA parser
     */
    public Receiver() {
        // set WsConnector
        Preferences pref = Preferences.getInstance();
        String strWsConnIndex = pref.get(Preferences.kWsConnector);
        if (strWsConnIndex != null) {
            if (strWsConnIndex.equals(Preferences.vHTTP)) {
                ws_client = new RobustRestfulClient();
            } else if (strWsConnIndex.equals(Preferences.vFile)) {
                ws_client = new FileConnector();
            }
        }
        // should not happen, unless the RecordStore for preference is inconsistent.
        if (ws_client == null) {
            ws_client = new RobustRestfulClient();
        }

        // set minimum valid speed
        String strProfileIndex = pref.get(Preferences.kProfile);
        int profileIndex;
        if ( strProfileIndex != null ) {
            profileIndex = Integer.parseInt(strProfileIndex);
            if ( profileIndex == 0 ) {
                // slow walker
                minValidSpeed = (float) 0.1;
            }else if( profileIndex == 1 ) {
                // walker
                minValidSpeed = (float) 0.5;
            }else if( profileIndex == 2 ) {
                // biker
                minValidSpeed = (float) 2;
            }else if( profileIndex == 3 ) {
                // driver
                minValidSpeed = (float) 2;
            }else{
                // default
                minValidSpeed = (float) 0.1;
            }
        }else{
            // default
            minValidSpeed = (float) 0.1;
        }

        // set auto-pause
        String vAutoPause = pref.get(Preferences.kAutoPause);
        if ( vAutoPause != null ) {
            autoPauseMode = vAutoPause.equals(Preferences.vYes);
        }else{
            autoPauseMode = true;
        }

        sem = new Object();
        initialized = false;
        startLat = -1;
    }

    /**
     * Called from <code>Gui</code> class
     */
    public void setGui(Gui gui) {
        this.gui = gui;
    }

    /**
     * Called from <code>Gui</code> class
     */
    public void setWsConnector(WsConnector wsconn) {
        this.ws_client = wsconn;
    }

    /**
     * Set GPS device connector
     */
    public void setDeviceConnector(DeviceConnector conn) {
        this.connector = conn;
    }

    /**
     * Get the runtime in milliseconds
     */
    public String getRuntime() {
        return Waypoint.timeMStoString(runTime, true, false);
    }

    /**
     * Prepares GPS connector and HTTP client for initialization
     */
    public void configure() {
        connector.configure();
        ws_client.configure();
    }

    /**
     * Initializes GPS connector and HTTP client.
     * Called right before thread start
     */
    public int initialize() {
        /* initialize GPS device connector */
        if (connector.initialize() < 1) {
            return -1;
        }
        /* initialize HTTP client */
        if (ws_client.initialize() < 1) {
            gui.setContent("Internet not available at the moment. GPS records will be stored. Pinkr will retry to send them in next execution.");
            return -1;
        }

//        wpBuffer = new WaypointsBuffer();

        receiving = false;
        sending = false;
        roundStarted = false;
        initialized = true;
        return 1;
    }

    /**
     * Start receiving GPS data.
     * Do nothing if the receiver is already active.
     * @return 1 if the status is changed from inactive to active,
     * 0 if the status is already active before.
     */
    public int activate() {
        if (receiving) {
            return 0;
        }
        connector.activate();
        receiving = true;
        sending = true;
        autoPaused = false;
        timer = new Timer();
        timer.schedule(this, 0, INTERVAL);
        tit = System.currentTimeMillis();
        spdTit = tit;
        return 1;
    }

    /**
     * Stop receiving GPS data
     * Do nothing if the receiver is already inactive.
     * @return 1 if the status is changed from active to inactive,
     * 0 if the status is already inactive before.
     */
    public int deactivate() {
        if (!receiving) {
            return 0;
        }
        receiving = false;
        sending = false;
        timer.cancel();
        // garbage
        timer = null;
        waitTilFinish();
        connector.deactivate();
        return 1;
    }

    /**
     * Start a round.
     * An action "start" will be sent.
     * The receiver will become active.
     * @return 1 if succeeded, 0 if already started before.
     */
    public int startRound() {
        roundStarted = true;
        // reset the timer
        runTime = 0;
        dist = 0.0;
        aveSpeed = 0;
        maxSpeed = 0;

        ws_client.pushAction("start");
//        wpBuffer.initialize();

        int res = activate();
        return res;
    }

    /**
     * Stop a round.
     * An action "stop" will be sent.
     * The receiver will become inactive.
     * @return 1 if succeeded, 0 if already stopped before.
     */
    public int stopRound() {
        roundStarted = false;
        int res = deactivate();
//        wpBuffer.terminate(ws_client);
        ws_client.pushAction("stop");
        return res;
    }

    /**
     * Resume a round.
     * An action "resume" will be sent.
     * The receiver will become active.
     * @return 1 if succeeded, 0 if already started before.
     */
    public int resumeRound() {
        int res = 1;
        ws_client.pushAction("resume");
        if ( autoPauseMode ) {
            sending = true;
        }else{
            res = activate();
        }
        return res;
    }

    /**
     * Pause a round.
     * An action "pause" will be sent.
     * The receiver will become inactive.
     * @return 1 if succeeded, 0 if already stopped before.
     */
    public int pauseRound() {
        int res = 1;
        if ( autoPauseMode ) {
            // ensure sending has stopped
            synchronized(sem){
                sending = false;
            }
        }else{
            res = deactivate();
        }
        ws_client.pushAction("pause");
        return res;
    }

    /**
     * Should be called from the main <code>Gui</code> class
     */
    public void terminate() {
        if ( !initialized ) {
            return;
        }

        initialized = false;
        // if the round is not stopped, stop it
        if (roundStarted) {
            stopRound();
        }
//        gui.showInfo("Sending the remaining data...", 3000);
        // finalized the waypoints buffer, i.e., send the last received waypoint.
//        wpBuffer.finalized(ws_client);
        if (connector.terminate() < 1) {
            gui.showError("Can't terminate GPS receiver!");
        }
        if (ws_client.terminate() < 1) {
            gui.showError("Can't terminate web service connection!");
        }
//        wpBuffer.terminate(ws_client);
    }

    /**
     * Receive waypoints from GPS connector and send/save them.
     */
    public void run() {
        synchronized (sem) {
            if (receiving) {
                long tat = System.currentTimeMillis();
                runTime += (int) (tat - tit);
                tit = tat;
//                    gui.setClock(Waypoint.timeMStoString(runTime, true, false) + '\n');
//                    String t = Waypoint.timeMStoString(runTime, false, false);
//                    ws_client.push("t=" + t + "&lat=46.0459664&lng=11.1334402&alt=277");
//                    gui.setContent(((RobustRestfulClient) ws_client).status());

                Waypoint wp;
                //gui.setCurrentBufferSize(data.getSize());
                //gui.output(parser.getInfo());
                wp = connector.getWaypoint();

                if (wp != null) {
                    // display the timing of the round
                    gui.set(Gui.K_CLOCK, Waypoint.timeMStoString(runTime, true, false));
                    // Display coords on the cell phone
//                    gui.setContent(wp.toString());
                    String [] ss = wp.toStrings();
                    gui.set(Gui.K_UTC, ss[0]);
                    gui.set(Gui.K_SPEED, ss[1]);
                    gui.set(Gui.K_COURSE, ss[2]);
                    gui.set(Gui.K_SATELLITE, ss[3]);
                    
                    String [] str = new String[3];
                    str[0] = ss[1] + "km/h";
                    str[1] = Waypoint.doubleToString(dist / 1000, 3) + "km";
                    if ( wp.course >= 0 && wp.course <= 360 ) {
                        gui.getCompass().setCourse(wp.course);
                        // if this is the first valid waypoint
                        if ( startLat < 0 ) {
                            startLat = wp.latitude;
                            startLon = wp.longitude;
                        } else {
                            int v[] = new int [2];
                            Waypoint.calcDistAndDirection( startLat, startLon, wp.latitude, wp.longitude, v );
                            str[2] = "from " + Waypoint.doubleToString(v[0], 0) + " m";
                            gui.getCompass().setDirectionFrom(v[1]);
                            gui.getCompass().setText(str);
                        }
                    }

                    //TODO send only valid waypoint
                    if (wp.valid()) {
                        // if we are not in autopaused mode, or we are sending
                        if ( !autoPauseMode || sending  ) {
                            // send the waypoint, must send before auto pause
                            ws_client.push(wp.toHttpQuery());
                            //                wpBuffer.update(ws_client, wp);

                            float spd = wp.speed;
                            if (!Float.isNaN(spd)) {
                                long spdTat = System.currentTimeMillis();
                                //// the same with: dist += (wp.speed / 3.6) * (( spdTat - spdTit ) / 1000);
                                dist += spd * (spdTat - spdTit) / 3600;
                                spdTit = spdTat;

                                // max speed
                                if (spd > maxSpeed) {
                                    maxSpeed = spd;
                                }
                            }

                                // auto-pause trigger
                                if ( autoPauseMode && spd < minValidSpeed ) {
                                    long bsTat = System.currentTimeMillis();
                                    // if the timer not started yet, start it
                                    if ( belowSpeedTit < 0 ) {
                                        belowSpeedTit = bsTat;
                                    }else if ( bsTat - belowSpeedTit > TIMEOUT_BELOW_SPEED ) {
                                        // time out
                                        // TODO check the status of the screen
//                                        gui.showInfo("auto paused", 3000);
                                        autoPaused = true;
                                        gui.pauseRound();
                                    }
                                }

                            // average speed
                            if (runTime > 0) {
                                // unit of runTime is millisecond, unit of aveSpeed is km/h
                                aveSpeed = (float) (1000 * 3.6 * dist / runTime);
                            }

                            gui.set(Gui.K_DIST, Waypoint.doubleToString(dist / 1000, 3));
                            gui.set(Gui.K_MAX_SPD, Waypoint.floatToString(maxSpeed, 1));
                            gui.set(Gui.K_AVE_SPD, Waypoint.floatToString(aveSpeed, 1));
                        }else{
                            // here, it must be autoPauseMode && !sending
                            if ( !Float.isNaN(wp.speed) && wp.speed > minValidSpeed && autoPaused ) {
                                // stop the timer
                                belowSpeedTit = -1;
                                // show info
                                // TODO check the status of the cover, if closed, it seems will block
//                                gui.showInfo("auto resumed", 3000);
                                // resume
                                autoPaused = false;
                                gui.resumeRound();
                            }
                        }
                    }

                    // DEBUG
//                    if (ws_client instanceof RobustRestfulClient) {
//                        // Display coords on the cell phone
//                        gui.setContent(wp.toString() + ((RobustRestfulClient) ws_client).status());
//                    } else {
//                        // Display coords on the cell phone
//                        gui.setContent(wp.toString());
//                    }
                }
            }
        }
    }

    /**
     * Wait til the receiver have stopped receiving waypoints
     */
    public void waitTilFinish() {
        synchronized (sem) {
            // active should be set to false before calling waitTilFinish()
            // here set it again, just for fault tolerance
            receiving = false;
        }
    }
}
